Regression Notebook¶

Ziel dieses letzten Notebooks ist es die zuvor gescrapten Daten zu kombinieren und auszuwerten. Dafür wird das bestmögliche Regressionsmodel zur Vorhersage des Kaufpreises von Häusern bestimmt. Dazu wird zuvor eine explorative Datenanalyse (EDA) durchgeführt, um Strukturen und Besonderheiten in den Daten zu erkennen und hervorzuheben. Danach werden die Daten für die Regression aufbereitet. Im Anschluss werden verschiedene Regressionsmodelle erstellt, optimiert und miteinander verglichen.

Ein weiterer Teil dieses Notebooks ist es die Bauzinsen einzubeziehen. Da wir nur einen kurzen Zeitraum betrachtet haben, in dem sich die Zinsen nicht stark verändert haben, ergibt es keinen Sinn, diese als Variable in die Regressionsanalyse aufzunehmen. Es wird trotzallem die Entwicklung des Bauzinses mit den Kaufpreisen über die Zeit betrachtet und auf mögliche Trends und Zusammenhänge untersucht.

Inhaltsverzeichnis¶

  1. Installation
  2. Explorative Datenanlyse
    1. Datenvorbereitung
      1. Nullwerte
      2. Duplikate
    2. EDA numerischer Features
      1. Analyse einzlner Features
      2. Korrelationen
    3. EDA kategorischer Features
      1. Analyse einzelner Features
      2. Korrelationen
    4. Zusammenhänge zwischen numerischen und kategorischen Variablen
  3. Regression
    1. Preprocessing
      1. One-Hot-Encoding
      2. Test-Train-Split
    2. Regressionsmodelle
      1. Statsmodel
      2. Sklearn
  4. Inventardauer der Inserate
  5. Verbindung mit Bauzinsen
    1. Verlauf Hauspreise zu Bauzinsen
  6. Fazit

1. Installationen¶

Als Voraussetzung zur Funktionstüchtigkeit des Codes, werden im ersten Schritt die benötigten Libraries erstellt. Diese werden für folgende Befehle benötigt:

  • Pandas: Grundlegende Untersuchung von Datensätzen
  • Numpy: Berechnungen und Umwandlungen
  • Seaborn und Plotly: Graphiken bei der EDA
  • Statsmodel und Sklearn: Regression
In [435]:
import pandas as pd
import numpy as np
import seaborn as sns
import plotly.express as px
import plotly.graph_objs as go
import plotly.figure_factory as ff
from scipy import stats
import statsmodels.formula.api as smf
from statsmodels.formula.api import ols
import statsmodels.api as sm
from time import time
from patsy import dmatrices
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn import svm
from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.linear_model import LinearRegression, BayesianRidge, SGDRegressor, LassoCV
from sklearn.svm import SVR
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.metrics import mean_absolute_error,  median_absolute_error
from sklearn.pipeline import Pipeline
import matplotlib.pyplot as plt

2. Explorative Datenanalyse¶

In diesem Kapitel werden die Häuserdaten analysiert und auf Strukturen, Besonderheiten und Zusammenhänge untersucht.

2.1. Datenvorbereitung¶

Als erstes wird die täglichen Immobililien csv-Dateien eingelesen, verknüpft und auf deren Korrektheit überprüft.

In [2]:
immo2812=pd.read_csv("daily_data//immo//2022-12-28_Immobilien.csv", engine='python')
immo2912=pd.read_csv("daily_data//immo//2022-12-29_Immobilien.csv", engine='python')
immo3012=pd.read_csv("daily_data//immo//2022-12-30_Immobilien.csv", engine='python')
immo3112=pd.read_csv("daily_data//immo//2022-12-31_Immobilien.csv", engine='python')
immo0101=pd.read_csv("daily_data//immo//2023-01-01_Immobilien.csv", engine='python')
immo0201=pd.read_csv("daily_data//immo//2023-01-02_Immobilien.csv", engine='python')
immo0301=pd.read_csv("daily_data//immo//2023-01-03_Immobilien.csv", engine='python')
immo0401=pd.read_csv("daily_data//immo//2023-01-04_Immobilien.csv", engine='python')
immo2812.head()
Out[2]:
Unnamed: 0 ID Ort Umkreis MaxOrt Preis Fläche Zimmer Grundstücksfläche Kategorie Etagen Baujahr Effizienzklasse Energieträger Heizungsart Stand
0 0 28zn659 berlin 50 max. 15 km 7.790.000 € ['780 m²'] ['30 Zi.'] ['1.696 m²'] ['Villa'] ['5 Geschosse'] ['1890'] n.a n.a n.a 2022-12-28
1 1 28jr359 berlin 50 max. 30 km 450.000 € ['111 m²'] ['5 Zi.'] ['1.100 m²'] ['Einfamilienhaus'] ['2 Geschosse'] ['1936'] ['H'] ['Gas, Kohle'] ['Zentralheizung'] 2022-12-28
2 2 274he5l berlin 50 max. 10 km 690.000 € ['323 m²'] ['8 Zi.'] ['393 m²'] ['Mehrfamilienhaus'] n.a ['1990'] ['D'] ['Gas'] ['Zentralheizung'] 2022-12-28
3 3 26zkl56 berlin 50 max. 10 km 429.000 € ['97 m²'] ['4 Zi.'] ['407 m²'] ['Einfamilienhaus'] n.a ['1958'] ['E'] ['Öl'] ['Zentralheizung'] 2022-12-28
4 4 279kt5y berlin 50 max. 10 km 1.700.000 € ['186.62 m²'] ['8 Zi.'] ['502 m²'] ['Mehrfamilienhaus'] n.a ['1936'] n.a ['Fernwärme'] ['Zentralheizung'] 2022-12-28
In [3]:
immodf = pd.concat([immo2812,immo2912,immo3012,immo3112,immo0101,immo0201,immo0301,immo0401],ignore_index=True)
print(immodf.shape)
immodf.head()
(55060, 16)
Out[3]:
Unnamed: 0 ID Ort Umkreis MaxOrt Preis Fläche Zimmer Grundstücksfläche Kategorie Etagen Baujahr Effizienzklasse Energieträger Heizungsart Stand
0 0 28zn659 berlin 50 max. 15 km 7.790.000 € ['780 m²'] ['30 Zi.'] ['1.696 m²'] ['Villa'] ['5 Geschosse'] ['1890'] n.a n.a n.a 2022-12-28
1 1 28jr359 berlin 50 max. 30 km 450.000 € ['111 m²'] ['5 Zi.'] ['1.100 m²'] ['Einfamilienhaus'] ['2 Geschosse'] ['1936'] ['H'] ['Gas, Kohle'] ['Zentralheizung'] 2022-12-28
2 2 274he5l berlin 50 max. 10 km 690.000 € ['323 m²'] ['8 Zi.'] ['393 m²'] ['Mehrfamilienhaus'] n.a ['1990'] ['D'] ['Gas'] ['Zentralheizung'] 2022-12-28
3 3 26zkl56 berlin 50 max. 10 km 429.000 € ['97 m²'] ['4 Zi.'] ['407 m²'] ['Einfamilienhaus'] n.a ['1958'] ['E'] ['Öl'] ['Zentralheizung'] 2022-12-28
4 4 279kt5y berlin 50 max. 10 km 1.700.000 € ['186.62 m²'] ['8 Zi.'] ['502 m²'] ['Mehrfamilienhaus'] n.a ['1936'] n.a ['Fernwärme'] ['Zentralheizung'] 2022-12-28

Das entstandene Dataframe hat 55060 Zeilen und 16 Spalten.

Bei der Betrachtung des heads von Dataframe immodf fällt auf, dass viele nicht benötigten Sonderzeichen und Buchstaben vohanden sind. Diese werden im nächsten Schritt entfernt und wenn reine Zahlenwerte übrig bleiben, werden diese auch in einen numerischen Datentyp umgewandelt.

In [4]:
#MaxOrt
immodf["MaxOrt"]=immodf["MaxOrt"].str.replace("max. ","") 
immodf["MaxOrt"]=immodf["MaxOrt"].str.replace(" km","") 
immodf["MaxOrt"]=immodf["MaxOrt"].str.replace(",",".") 
immodf.loc[:,"MaxOrt"]=pd.to_numeric(immodf["MaxOrt"]) #in numerischen Datentyp umwandeln

#Preis
immodf["Preis"]=immodf["Preis"].str.replace(".","")
immodf["Preis"]=immodf["Preis"].str.replace(" €","")
immodf["Preis"]=immodf["Preis"].str.replace("auf Anfrage","0") 
immodf["Preis"] = immodf["Preis"].apply(lambda x: x.split(',')[0]) #Ganzzahlen reichen aus
immodf.loc[:,"Preis"]=pd.to_numeric(immodf["Preis"])

#Fläche
immodf['Flaeche'] = immodf['Fläche'].apply(lambda x: x[1:-1]) #neue Spalte generieren, dass Listenmodul aufgelöst wird
immodf['Flaeche'] = immodf['Flaeche'].str.replace(" m²","")
immodf['Flaeche'] = immodf['Flaeche'].str.replace(".","")
immodf['Flaeche'] = immodf['Flaeche'].str.replace("'","")
immodf.loc[:,"Flaeche"]=pd.to_numeric(immodf["Flaeche"])
immodf = immodf.drop('Fläche', axis=1) #alte Spalte löschen

#Zimmer 
immodf['Raeume'] = immodf['Zimmer'].apply(lambda x: x[1:-1])
immodf['Raeume'] = immodf['Raeume'].str.replace(" Zi","")
immodf['Raeume'] = immodf['Raeume'].str.replace(".","")
immodf['Raeume'] = immodf['Raeume'].str.replace("'","")
immodf.loc[:,'Raeume']=pd.to_numeric(immodf['Raeume'])
immodf = immodf.drop('Zimmer', axis=1)

#Grundstücksfläche
immodf['Grundstuecksflaeche'] = immodf['Grundstücksfläche'].apply(lambda x: x[1:-1])
immodf['Grundstuecksflaeche'] = immodf['Grundstuecksflaeche'].str.replace(" m²","")
immodf['Grundstuecksflaeche'] = immodf['Grundstuecksflaeche'].str.replace(".","")
immodf['Grundstuecksflaeche'] = immodf['Grundstuecksflaeche'].str.replace("'","")
immodf['Grundstuecksflaeche'] = immodf['Grundstuecksflaeche'].str.replace(",",".")
immodf['Grundstuecksflaeche']=immodf['Grundstuecksflaeche'].str.replace("kA","0")
immodf.loc[:,"Grundstuecksflaeche"]=pd.to_numeric(immodf["Grundstuecksflaeche"])
immodf = immodf.drop('Grundstücksfläche', axis=1)

#Kategorie
immodf['Art'] = immodf['Kategorie'].apply(lambda x: x[1:-1])
immodf['Art'] = immodf['Art'].str.replace("'","")
immodf['Art'] = immodf['Art'].str.replace(".","")
immodf = immodf.drop('Kategorie', axis=1)

#Etagen
immodf['Geschosse'] = immodf['Etagen'].apply(lambda x: x[1:-1])
immodf['Geschosse'] = immodf['Geschosse'].str.replace(" Geschosse","")
immodf['Geschosse'] = immodf['Geschosse'].str.replace(" Geschoss","")
immodf['Geschosse'] = immodf['Geschosse'].str.replace("'","")
immodf['Geschosse'] = immodf['Geschosse'].str.replace("n.a","0")
immodf['Geschosse'] = immodf['Geschosse'].str.replace(".","")
immodf.loc[:,"Geschosse"]=pd.to_numeric(immodf["Geschosse"])
immodf = immodf.drop('Etagen', axis=1)

#Baujahr
immodf['Jahr'] = immodf['Baujahr'].apply(lambda x: x[1:-1])
immodf['Jahr'] = immodf['Jahr'].str.replace("'","")
immodf['Jahr'] = immodf['Jahr'].str.replace("(","")
immodf['Jahr'] = immodf['Jahr'].str.replace("n.a","0")
immodf['Jahr'] = immodf['Jahr'].str.replace(".","")
immodf['Jahr'] = immodf['Jahr'].str.replace("ca ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("ca","")
immodf['Jahr'] = immodf['Jahr'].str.replace("um ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("saniert ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("3 Quartal ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("Baubeginn ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("Neubau","2023")
immodf['Jahr'] = immodf['Jahr'].str.replace("renoviert ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("vor ","")
immodf['Jahr'] = immodf['Jahr'].str.replace(" 1997 umfa","")
immodf['Jahr'] = immodf['Jahr'].str.replace("unbekannt ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("nicht bek ","")
immodf['Jahr'] = immodf['Jahr'].str.replace("nicht bekannt","")
immodf['Jahr'] = immodf['Jahr'].str.replace(" 2023","")
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split('/')[0])
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split(' -')[0])
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split('-')[0])
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split(',')[0])
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split(' und')[0])
immodf['Jahr'] = immodf['Jahr'].apply(lambda x: x.split(' +')[0])
immodf.loc[:,"Jahr"]=pd.to_numeric(immodf["Jahr"])
immodf = immodf.drop('Baujahr', axis=1)

#Effizienzklasse
immodf['Effizienz'] = immodf['Effizienzklasse'].apply(lambda x: x[1:-1])
immodf['Effizienz'] = immodf['Effizienz'].str.replace("'","")
immodf['Effizienz'] = immodf['Effizienz'].str.replace(".","")
immodf = immodf.drop('Effizienzklasse', axis=1)

#Energieträger
immodf['Energietraeger'] = immodf['Energieträger'].apply(lambda x: x[1:-1])
immodf['Energietraeger'] = immodf['Energietraeger'].str.replace("'","")
immodf['Energietraeger'] = immodf['Energietraeger'].str.replace(".","")
immodf = immodf.drop('Energieträger', axis=1)

#Heizungsart
immodf['Heizung'] = immodf['Heizungsart'].apply(lambda x: x[1:-1])
immodf['Heizung'] = immodf['Heizung'].str.replace("'","")
immodf['Heizung'] = immodf['Heizung'].str.replace(".","")
immodf = immodf.drop('Heizungsart', axis=1)

#Unnütze Variablen entfernen
immodf = immodf.drop('Unnamed: 0', axis=1) #nur ein Zählwert, schon durch Zeilennummer gegeben
immodf = immodf.drop('Umkreis', axis=1) #immer 50, da ein Umkreis von 50 km um jede Stadt betrachtet wird
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:2: FutureWarning: The default value of regex will change from True to False in a future version.
  immodf["MaxOrt"]=immodf["MaxOrt"].str.replace("max. ","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:8: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf["Preis"]=immodf["Preis"].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:17: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf['Flaeche'] = immodf['Flaeche'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:25: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf['Raeume'] = immodf['Raeume'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:33: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf['Grundstuecksflaeche'] = immodf['Grundstuecksflaeche'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:43: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf['Art'] = immodf['Art'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:51: FutureWarning: The default value of regex will change from True to False in a future version.
  immodf['Geschosse'] = immodf['Geschosse'].str.replace("n.a","0")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:52: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf['Geschosse'] = immodf['Geschosse'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:59: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf['Jahr'] = immodf['Jahr'].str.replace("(","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:60: FutureWarning: The default value of regex will change from True to False in a future version.
  immodf['Jahr'] = immodf['Jahr'].str.replace("n.a","0")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:61: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf['Jahr'] = immodf['Jahr'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:88: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf['Effizienz'] = immodf['Effizienz'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:94: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf['Energietraeger'] = immodf['Energietraeger'].str.replace(".","")
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/4288838531.py:100: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  immodf['Heizung'] = immodf['Heizung'].str.replace(".","")

Zur Überprüfung, ob die Daten nun den erwünschten Format entsprechen, wird der Kopf und die Info des dataframes angezeigt.

In [5]:
immodf.head()
Out[5]:
ID Ort MaxOrt Preis Stand Flaeche Raeume Grundstuecksflaeche Art Geschosse Jahr Effizienz Energietraeger Heizung
0 28zn659 berlin 15.0 7790000 2022-12-28 780.0 30.0 1696.0 Villa 5.0 1890.0
1 28jr359 berlin 30.0 450000 2022-12-28 111.0 5.0 1100.0 Einfamilienhaus 2.0 1936.0 H Gas, Kohle Zentralheizung
2 274he5l berlin 10.0 690000 2022-12-28 323.0 8.0 393.0 Mehrfamilienhaus NaN 1990.0 D Gas Zentralheizung
3 26zkl56 berlin 10.0 429000 2022-12-28 97.0 4.0 407.0 Einfamilienhaus NaN 1958.0 E Öl Zentralheizung
4 279kt5y berlin 10.0 1700000 2022-12-28 18662.0 8.0 502.0 Mehrfamilienhaus NaN 1936.0 Fernwärme Zentralheizung
In [6]:
immodf.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 55060 entries, 0 to 55059
Data columns (total 14 columns):
 #   Column               Non-Null Count  Dtype  
---  ------               --------------  -----  
 0   ID                   55060 non-null  object 
 1   Ort                  55060 non-null  object 
 2   MaxOrt               55060 non-null  float64
 3   Preis                55060 non-null  int64  
 4   Stand                55060 non-null  object 
 5   Flaeche              54515 non-null  float64
 6   Raeume               51591 non-null  float64
 7   Grundstuecksflaeche  54842 non-null  float64
 8   Art                  55060 non-null  object 
 9   Geschosse            9822 non-null   float64
 10  Jahr                 48476 non-null  float64
 11  Effizienz            55060 non-null  object 
 12  Energietraeger       55060 non-null  object 
 13  Heizung              55060 non-null  object 
dtypes: float64(6), int64(1), object(7)
memory usage: 5.9+ MB

Es gibt insgesamt 55060 Zeilen, also zu verkaufende Häuser, die betrachtet werden. Die verwendeten 14 Variablen sind folgende: Column Name | Description | Data Type :------------------------ | :---------------- |:-----------------: ID | Einzigartige Identifikationsnummer des Hauses | Object Ort | Ortsname, in/um den das Haus steht | Object MaxOrt | Maximaler Abstand zum Ort in km | Integer Preis | Hauspreis | Integer Stand | Datum des Scrapings | Object Flaeche | Wohnfläche in m2 | Float Raeume | Anzahl der Zimmer im Haus | Float Grundstuecksflaeche | Grundstücksfläche in m2 | Float Art | Kategorie des Hauses | Object Geschosse | Anzahl der Stockwerke im Haus | Float Jahr | Baujahr des Hauses | Float Effizienz | Effizienzklasse (A++ - H) | Object Energietraeger | Verwendete Stoffe zur Energieerzeugung | Oject Heizung | Art der verbauten Heizung | Object

2.1.1. Nullwerte¶

Aus der Info lässt sich ablesen, dass einige Nullwerte vorhanden sind. Dies wird nun visualisiert für eine einfachere Entscheidung, ob diese Zeilen entfernt werden. Nullwerte im folgenden Graphen in gelb angezeigt. Es werden nur numerische Spalten angezeigt.

In [7]:
sns.heatmap(immodf.isnull(), yticklabels=False, cbar=False, cmap='viridis')
Out[7]:
<AxesSubplot:>

Bei Geschosse sind sehr viele Nullwerte vorhanden. Ein Löschen all dieser Zeilen im dataframe, würde dieses zu stark verkleinern, daher werden alle Zeilen mit Nullwerten beibehalten und durch den Mittelwert (bei numerischen Variablen) oder dem nächsten Wert (bei kategorischen Variablen) aller Einträge ersetzt.

Bei der manuellen Sichtung der csv-Dateien, ist in einer aufgefallen, dass am Ende einige Zeilen komplett mit Nullwerten sind. Diese werden ebenfalls entfernt.

In [8]:
#für numerische Spalten
immodf=immodf.fillna(immodf.mean()) 

#für kategorische Spalten
immodf = immodf[immodf.Ort != "0"]
immodf.loc[immodf.Effizienz == "", 'Effizienz'] = np.nan
immodf.loc[immodf.Energietraeger == "", 'Energietraeger'] = np.nan
immodf.loc[immodf.Heizung == "", 'Heizung'] = np.nan
immodf.loc[immodf.Art == "", 'Art'] = np.nan
immodf = immodf.fillna(method="bfill") #bfill umd durch nächsten Wert zu ersetzen

immodf.head()
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/3599110604.py:2: FutureWarning: Dropping of nuisance columns in DataFrame reductions (with 'numeric_only=None') is deprecated; in a future version this will raise TypeError.  Select only valid columns before calling the reduction.
  immodf=immodf.fillna(immodf.mean())
Out[8]:
ID Ort MaxOrt Preis Stand Flaeche Raeume Grundstuecksflaeche Art Geschosse Jahr Effizienz Energietraeger Heizung
0 28zn659 berlin 15.0 7790000 2022-12-28 780.0 30.0 1696.0 Villa 5.000000 1890.0 H Gas, Kohle Zentralheizung
1 28jr359 berlin 30.0 450000 2022-12-28 111.0 5.0 1100.0 Einfamilienhaus 2.000000 1936.0 H Gas, Kohle Zentralheizung
2 274he5l berlin 10.0 690000 2022-12-28 323.0 8.0 393.0 Mehrfamilienhaus 2.417838 1990.0 D Gas Zentralheizung
3 26zkl56 berlin 10.0 429000 2022-12-28 97.0 4.0 407.0 Einfamilienhaus 2.417838 1958.0 E Öl Zentralheizung
4 279kt5y berlin 10.0 1700000 2022-12-28 18662.0 8.0 502.0 Mehrfamilienhaus 2.417838 1936.0 D Fernwärme Zentralheizung

2.1.2. Duplikate¶

Wir vermuten, dass viele Häuser über mehrere Tage inseriert sind und somit mehrfach vorkommen. Für die Regression soll jedes Inserat jedoch nur ein Mal gelistet sein, daher werden die Duplikate entfernt. Außerdem wird überprüft, ob Inserate mehrfach unter unterschiedlicher ID vorkommen. Diese identifizierten Duplikate werden ebenfalls entfernt. Es wird ein neues Dataframe df erstellt, indem die Duplikate entfernt sind. Für die Berechnung der Inventardauer der Inserate werden die Duplikate und somit immodf noch benötigt.

In [9]:
for col in immodf.columns:
    values = immodf[col].unique()
    print(col, "has", len(immodf[col].unique()), "unique values")
ID has 7963 unique values
Ort has 7 unique values
MaxOrt has 10 unique values
Preis has 1533 unique values
Stand has 6 unique values
Flaeche has 1942 unique values
Raeume has 72 unique values
Grundstuecksflaeche has 1734 unique values
Art has 14 unique values
Geschosse has 8 unique values
Jahr has 202 unique values
Effizienz has 11 unique values
Energietraeger has 60 unique values
Heizung has 35 unique values

Das Dataframe hat 55060 Zeilen, jedoch nur 7963 einzigartige Werte für ID.

In [10]:
df = immodf.drop_duplicates(subset=["ID"], keep='first')
df = immodf.drop_duplicates(subset=["Ort", "MaxOrt", "Preis", "Flaeche", "Raeume", "Grundstuecksflaeche", "Art", "Energietraeger", "Heizung"], keep='first')
df.shape
Out[10]:
(8169, 14)

Nach dem Entfernen von Duplikaten bestehen noch 8169 der ursprünglich 55060 Häuser im Dataframe.

2.2. EDA numerischer Features¶

In diesem Kapitel werden die numerischen Features im dataframe untersucht. Zuerst werden diese einzeln auf Ausprägungen und Verteilungen analysiert, danach auch deren Zusammenhänge.

Welche numerischen Features gibt es in df?

In [11]:
numeric_features=df.select_dtypes(include=np.number).columns.to_list()
numeric_features
Out[11]:
['MaxOrt',
 'Preis',
 'Flaeche',
 'Raeume',
 'Grundstuecksflaeche',
 'Geschosse',
 'Jahr']

Es gibt 7 numerische Features in df. Deren Ausprägungen werden nun analysiert.

2.2.1. Analyse einzelner Features¶

In [12]:
df.describe().applymap('{:,.2f}'.format)
Out[12]:
MaxOrt Preis Flaeche Raeume Grundstuecksflaeche Geschosse Jahr
count 8,169.00 8,169.00 8,169.00 8,169.00 8,169.00 8,169.00 8,169.00
mean 20.48 1,054,410.19 3,410.98 11.00 733.42 2.42 1,967.25
std 13.03 1,399,783.44 10,733.07 17.40 3,705.69 0.38 54.46
min 0.50 0.00 1.00 1.00 0.00 1.00 23.00
25% 10.00 495,617.00 133.00 5.00 259.50 2.42 1,955.00
50% 15.00 725,000.00 180.00 6.00 460.00 2.42 1,967.56
75% 30.00 1,143,000.00 519.00 9.00 735.00 2.42 1,995.00
max 50.00 30,000,000.00 307,673.00 415.00 195,940.00 8.00 2,024.00

MaxOrt: Der geringste maximale Abstand zum Stadtzentrum sind 0,5 km und der höchste 50 km. Der Median liegt bei 15 km und der Mittelwert bei 20 km. MaxOrt scheint daher recht gleichmäßig verteilt zu sein, wobei die meisten Häuser eher näher am Stadtzentrum liegen.

Preis: Das erste Quartil endet bei knapp 0,5 Millionen € und das Dritte bei knapp über einer Million €. Der Median liegt bei 725.000€ und der Mittelwert bei über einer Million €. Dies weist daraufhin, dass es einige sehr teure Häuser, bis zu 30 Mio. € gibt, die den Mittelwert nach oben ziehen. Dabei handelt es sich vermutlich um ein paar Ausreißer.

Flaeche: Das erste Quartil endet bei 133 m2 und das Dritte bei 519 m2. Der Median liegt bei 180 m2 und der Mittelwert bei knapp 3500 m2. Dies weist daraufhin, dass es einige sehr große Häuser, bis zu über 300.000 m2 gibt, die den Mittelwert nach oben ziehen. Dabei handelt es sich vermutlich um ein paar Ausreißer.

Raume: Das erste Quartil endet bei 5 Zimmern und das Dritte bei 9 Zimmern. Der Median liegt bei 6 Zimmern und der Mittelwert bei 11 Zimmern. Dies weist daraufhin, dass es einige Häuser mit sehr vielen Zimmern bis zu 415 Zimmern gibt, die den Mittelwert nach oben ziehen. Dabei handelt es sich vermutlich um ein paar Ausreißer.

Grundstuecksflaeche: Das erste Quartil endet bei 259 m2 und das Dritte bei 735 m2. Der Median liegt bei 460 m2 und der Mittelwert bei 733 m2. Dies weist daraufhin, dass es einige Häuser mit sehr großen Grundstück bis zu knapp 200.000 m2 gibt, die den Mittelwert nach oben ziehen. Dabei handelt es sich vermutlich um ein paar Ausreißer.

Geschosse: Das erste bis Dritte Quartil, sowie der mittelwert liegen 2,4 Stockwerken. Dies ist auf die große Anzahl an Nullwerten zurückzuführen.

Jahr: Das erste Quartil endet beim Jahr 1955 und das Dritte bei 1995. Der Median und Mittelwert liegen bei 1967. Die Jahre scheinen recht gleichmäßig verteilt zu sein, mit ein paar Ausreißern zum Minimum vom Jahr 23.

Nach diesem Überblick, werden die Lagemaße der Features visualisiert und auf ggf. weitere Erkenntnisse zu den einzelnen Variablen eingegangen.

In [13]:
#Preis
fig = px.histogram(df, x="Preis", nbins=100,
                   marginal="box",
                   hover_data=["Ort", "ID"])
fig.show()

Es handelt sich tatsächlich um einige Ausreißer mit Preisen zwischen 2 und 30 Millionen €.

In [14]:
#MaxOrt
fig = px.histogram(df, x="MaxOrt", nbins=20,
                   marginal="box",
                   hover_data=["Ort", "ID"])
fig.show()

Die meisten Häuser liegen zwischen 8 und 22 km vom Stadtzentrum entfernt.

In [15]:
#Flaeche
fig = px.histogram(df, x="Flaeche", nbins=100,
                   marginal="box",
                   hover_data=["Ort", "ID"])
fig.show()

Es handelt sich bereits um Ausreißer ab einer Wohnfläche von 1100 m2. Es gibt jedoch einige Ausreißer.

In [16]:
#Raeume
fig = px.histogram(df, x="Raeume", nbins=100,
                   marginal="box",
                   hover_data=["Ort", "ID"])
fig.show()

Es handelt sich bereits um Ausreißer ab 15 Räumen je Haus. Es gibt jedoch einige Ausreißer.

In [17]:
#Grundstuecksflaeche
fig = px.histogram(df, x="Grundstuecksflaeche", nbins=100,
                   marginal="box",
                   hover_data=["Ort", "ID"])
fig.show()

Es handelt sich bereits um Ausreißer ab einer Grundstücksfläche von 1447 m2. Es gibt jedoch einige Ausreißer.

In [18]:
#Geschosse
fig = px.histogram(df, x="Geschosse", nbins=8,
                   marginal="box",
                   hover_data=["Ort", "ID"])
fig.show()

Mit Abstand die meisten Häuser haben 2 Stockwerke, welches den Median und Mittelwert von 2,4 begründet.

In [19]:
#Jahr
fig = px.histogram(df, x="Jahr", nbins=100,
                   marginal="box",
                   hover_data=["Ort", "ID"])
fig.show()

Es handelt sich um Ausreißer bei Häusern, die vor 1895 gebaut wurden. Die Exposes der extremen Ausreißer wurden betrachtet und die Zeilen werden entfernt.

In [20]:
df = df.query('Jahr > 200')

2.2.2. Korrelationen¶

Die Korrelationsmatrix gibt den linearen Zusammenhang zweier numerischer Features an. Ein Wert von 0 bedeutet dabei keinen linearen Zusammenhang zwischen den Variablen, 1 den stärksten positiven und -1 den stärksten negativen linearen Zusammenhang. Bei Werten von größer als 0,5 oder kleiner als -0,5 wird von einem starkem linearen Zusammenhang gesprochen.

Die Korrelationsmatrix beurteilt ausschließlich lineare Zusammenhänge. Das bedeutet, dass trotz geringem Wert in der Korrelationsmatrix ein (nicht linearer) Zusammenhang bestehen kann, aber nicht muss.

In der gewählten Darstellung werden die Korrelationen sowohl farblich als auch numerisch angegeben. Wichtig für diese Analyse sind die Zusammenhänge mit einer bestimmten Variable, also welche anderen Variablen das gewählte Feature am meisten beinflussen. Es kann daher entweder die Zeile oder Spalte von des gewählten Features angeschaut werden und nach den stärksten Zusammenhängen untersucht werden.

Erwartungen:

  1. Stark positive Korrelation zwischen Preis und Flaeche und Grundstuecksflaeche, also größere Häuser und Grundstuecke sind teurer.
  2. Stark negative Korrelation zwischen Preis und MaxOrt, also näher am Zentrum gelegene (kleinere Distanz) Häuser sind teurer.
  3. Stark positive Korrelation zwischen Flaeche und Raeume, also größere Häuser haben mehr Zimmer.
In [21]:
corr = df.corr()
sns.heatmap(corr, cmap="Blues", annot=True)
Out[21]:
<AxesSubplot:>

Ergebnisse:

  1. Positive Korrelation von beiden Variablen zu Preis zu verzeichnen, jedoch nicht stark: Flaeche bei 0,19 und Grundstuecksflaeche bei 0,12.
  2. Stärkste (negative) Korrelation zu Preis hat MaxOrt, aber mit -0,22 nicht besonders stark ausgeprägt.
  3. Korrelation von Fleache zu Raeume mit 0,07 nicht bedeutend.

Als nächstes werden die Zusammenhänge der numerischen Features noch in einem Scatterplot betrachtet, dass nicht lineare Zusammenhänge visuell herausgefunden werden können.

In [22]:
#Scattermatrix
fig = px.scatter_matrix(df[numeric_features],height=800)
fig.update_traces(dict(opacity=0.3,marker=go.splom.Marker(size=3))
                 )
fig.show("notebook")

Aus der Scattermatrix lassen sich keine weiteren Zusammenhänge mit Preis ablesen, welche nicht linearer Form sind.

2.3. EDA kategorischer Features¶

In diesem Kapitel werden wie zuvor die numerischen, nun die kategorischen Features im Dataframe untersucht. Zuerst werden diese einzeln auf Ausprägungen und Verteilungen analysiert, danach auch deren Zusammenhänge.

Welche kategorischen Features gibt es in df?

In [23]:
cat_features=df.select_dtypes(exclude=np.number).columns.to_list()
cat_features
Out[23]:
['ID', 'Ort', 'Stand', 'Art', 'Effizienz', 'Energietraeger', 'Heizung']

Es gibt sieben kategorische Features, wovon eines die einzigartige ID ist. Für einen ersten Überblick über die Features, wird ein parellel categories plot verwendet. Dieser zeigt welche Ausprägungen und in welcher Konfiguration vorkommen.

In [24]:
fig = px.parallel_categories(df[cat_features])
fig.show("notebook")

Die sieben Orte sind recht gleichmäßig verteilt und am häufigsten stehen Einfamilienhäuser und Häuser mit einer Zentralheizung als Heizungsart zu Verkauf.

2.3.1. Analyse einzelner Features¶

In [25]:
#Wie viele Häuser werden je Stadt betrachtet?
df["Ort"].value_counts()
Out[25]:
hamburg              1214
koeln                1195
frankfurt-am-main    1183
berlin               1160
muenchen             1157
stuttgart            1139
leipzig              1118
Name: Ort, dtype: int64

Die Anzahl der Häuser je Stadt ist sehr änhlich, am meisten sind jedoch in Hamburg und am wenigsten in Leipzig.

In [26]:
#Art
df["Art"].value_counts()[:5].plot(kind="bar")
Out[26]:
<AxesSubplot:>

Die häufigste Art von Häusern sind Einfamilienhäusern, gefolgt von Mehrfamilienhäusern, sowie Doppelhaushälften und Reihenhäusern.

In [27]:
#Effizienz
df["Effizienz"].value_counts().plot(kind="bar")
Out[27]:
<AxesSubplot:>

Die häufigste Effizienzklasse ist H, die zweitschlechteste F, gefolgt von D, E und G. Gute Effizienzklassen hingegen sind deutlich seltener.

In [28]:
#Heizung
df["Heizung"].value_counts()[:5].plot(kind="bar")
Out[28]:
<AxesSubplot:>

Mit Abstand die häufigste Heizungsart ist die Zentralheizung. Mit nur rund einem Fünfteltel davon gefolgt sind die Kombination Zentralheizung und offener Kamin, sowie die Fußbodenheizung.

In [29]:
#Energietraeger
df["Energietraeger"].value_counts()[:5].plot(kind="bar")
Out[29]:
<AxesSubplot:>

Der häufigste Energieträger ist Gas und am zweithäufigsten, aber nur halb so oft vorkommend ist Öl. Alle anderen Energieträger haben einen sehr kleinen Anteil.

2.3.2. Korrelationen¶

Nun werden die kategorischen Features mit einander in Verbindung gebracht. Zuerst werden conditional probabilities betrachet, bei denen zwei Variablen und deren gemeinsames Auftreten als Häufigkeit in tabellarischer Form angegeben werden.

In [30]:
#Ort und Art
pd.crosstab(df["Ort"],df["Art"], normalize = "columns")
Out[30]:
Art Bauernhaus Bungalow Burg/Schloss Doppelhaushälfte Einfamilienhaus Finca Herrenhaus Mehrfamilienhaus Reihenendhaus Reihenmittelhaus Rustico Stadthaus Villa Wohn- und Geschäftshaus
Ort
berlin 0.068182 0.163180 0.000000 0.116134 0.197913 0.0 0.4 0.097475 0.101868 0.106667 0.0 0.052083 0.196154 0.2
frankfurt-am-main 0.113636 0.129707 0.000000 0.120306 0.130783 0.0 0.2 0.189665 0.159593 0.112222 0.0 0.083333 0.269231 0.1
hamburg 0.136364 0.267782 0.000000 0.118915 0.155826 0.0 0.2 0.099824 0.149406 0.188889 0.0 0.437500 0.203846 0.1
koeln 0.022727 0.150628 0.666667 0.160640 0.134957 0.0 0.0 0.167352 0.157895 0.157778 0.0 0.072917 0.034615 0.1
leipzig 0.500000 0.125523 0.333333 0.077191 0.171130 1.0 0.0 0.165003 0.040747 0.120000 0.0 0.135417 0.126923 0.1
muenchen 0.090909 0.075314 0.000000 0.251043 0.093913 0.0 0.0 0.081033 0.256367 0.185556 1.0 0.125000 0.130769 0.0
stuttgart 0.068182 0.087866 0.000000 0.155772 0.115478 0.0 0.2 0.199648 0.134126 0.128889 0.0 0.093750 0.038462 0.4

Die häufigste Hausart Einfamilienhaus ist in allen Städten verfügbar, am häufigsten jedoch in Leipzig und Berlin. 50% der Bauernhäuser liegen rund um Leipzig und 44% der inserierten Stadthäuser sind in Hamburg.

In [31]:
#Art und Effizienz
pd.crosstab(df["Art"],df["Effizienz"], normalize = "columns")
Out[31]:
Effizienz A A+ B C D E E, H F G H
Art
Bauernhaus 0.002174 0.000000 0.009225 0.007075 0.005978 0.000856 0.000 0.006240 0.004090 0.009181
Bungalow 0.017391 0.020050 0.016605 0.029481 0.023911 0.026541 0.250 0.035881 0.032720 0.038256
Burg/Schloss 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000 0.000000 0.001022 0.001530
Doppelhaushälfte 0.223913 0.270677 0.162362 0.192217 0.175064 0.179795 0.000 0.162246 0.159509 0.149197
Einfamilienhaus 0.336957 0.325815 0.324723 0.331368 0.343296 0.363014 0.125 0.326833 0.380368 0.394032
Finca 0.000000 0.000000 0.001845 0.000000 0.000000 0.000856 0.000 0.000000 0.000000 0.000000
Herrenhaus 0.000000 0.000000 0.000000 0.001179 0.000000 0.000000 0.000 0.000780 0.000000 0.002295
Mehrfamilienhaus 0.123913 0.110276 0.186347 0.182783 0.226302 0.203767 0.375 0.251950 0.216769 0.233359
Reihenendhaus 0.063043 0.087719 0.099631 0.067217 0.057216 0.077055 0.000 0.072543 0.074642 0.068860
Reihenmittelhaus 0.104348 0.117794 0.147601 0.132075 0.123826 0.121575 0.250 0.100624 0.099182 0.074981
Rustico 0.002174 0.000000 0.000000 0.001179 0.000000 0.000000 0.000 0.000000 0.000000 0.000000
Stadthaus 0.019565 0.022556 0.020295 0.008255 0.013664 0.005993 0.000 0.010140 0.015337 0.006886
Villa 0.106522 0.037594 0.031365 0.044811 0.029889 0.020548 0.000 0.030421 0.016360 0.020658
Wohn- und Geschäftshaus 0.000000 0.007519 0.000000 0.002358 0.000854 0.000000 0.000 0.002340 0.000000 0.000765

Eine sehr gute Effizienzklasse, A oder A+, ist vor allem in Einfamilienhäusern und Doppelhaushälften zu finden. Eine sehr schlechte Effizienzklasse weisen jedoch ebenso Einfamilienhäuser auf, aber besonders Mehrfamilienhäuser und Doppelhaushälften.

2.4. Zusammenhänge zwischen numerischen und kategorischen Variablen¶

Wie unterscheiden sich die numerischen Variablen je Stadt (nach Median)?

In [32]:
df.groupby(by="Ort").median().applymap('{:,.2f}'.format)
Out[32]:
MaxOrt Preis Flaeche Raeume Grundstuecksflaeche Geschosse Jahr
Ort
berlin 15.00 749,000.00 169.50 5.00 562.00 2.42 1,972.00
frankfurt-am-main 15.00 749,000.00 185.00 6.00 415.00 2.42 1,967.56
hamburg 15.00 695,000.00 161.50 5.00 550.50 2.42 1,969.00
koeln 15.00 619,000.00 167.00 6.00 432.00 2.42 1,968.00
leipzig 30.00 395,000.00 190.50 6.00 583.00 2.42 1,967.56
muenchen 15.00 1,295,000.00 200.00 6.00 387.23 2.42 1,981.00
stuttgart 20.00 750,000.00 199.00 8.00 368.00 2.42 1,967.56

Am teuersten sind mit Abstand Häuser in München, dieser Unterschied wird im folgenden Plot auch visualisiert.

Am weitesten vom Zentrum entfernt sind im Schnitt die Häuser in Leipzig und Stuttgart. In Stuttgart haben die Häuser auch die meisten Zimmer, im Durchschnitt acht. Die größte Wohnfläche und kleinste Grundstücksfläche haben Häuser in Stuttgart und München. Die Anzahl der Geschosse ist im Durchschnitt überall gleich, da es hier sehr viele Nullwerte gab und diese auf den Median gesetzt wurden. Die eher neuen Häuser, mit einem Median vom Jahr 1981, gibt es in München.

In [33]:
#Preis je Stadt
grouped = df.loc[:,['Ort', 'Preis']] \
    .groupby(['Ort']) \
    .median() \
    .sort_values(by='Preis')

sns. set_theme(style="whitegrid")
sns.set(rc={'figure.figsize':(11,7)})
ax=sns.boxplot(x=df.Ort, y=df.Preis, order=grouped.index)
ax.set_xticklabels(ax.get_xticklabels(),rotation=90)
ax.set(ylim=(0, 3000000))
ax
Out[33]:
<AxesSubplot:xlabel='Ort', ylabel='Preis'>

Der Median der Preise in Leipzig liegt sogar unterhalb der 25% günstigsten Häuser in allen anderen betrachteten Städte. Auch die Whisker sind vergleichweise kurz, so gibt es in Leipzig fast nur Ausreißer mit einem Preis höher als einer Million €. Auch in Köln und Stuttgart sind die Whisker und die Box eher näher beieinander. Die größte Preisspanne gibt es in München.

In [34]:
#Sind Häuser besserer Effizienzklasse eher teurer und in bestimmten Städten zu finden?
fig=px.scatter(df,x="Ort",y="Preis",color="Effizienz", range_y=(0, 2000000),
              hover_data=["ID","Ort"],title="Zusammenhang Preis, Ort und Effizienz")
fig.show("notebook")

Im oben stehenden Plot können in der Legende die Effizienzklassen augewählt (aktiviert/desktiviert) werden. Leider ist keine Tendenz zu unserer Vermutung, dass Häuser mit guter Effizienzklasse eher mehr kosten, zu verzeichnen.

In [35]:
#Wie hängt die Effizienzklasse mit dem Energietraeger zusammen?
fig = px.density_heatmap(df, x="Energietraeger", y="Effizienz",
                         marginal_x="histogram", marginal_y="histogram")
fig.show("notebook")

Es gibt keine klare Korrelation zwischen Effizienzklasse und Energieträger, da mehrere Energieträger, wie Gas, Öl oder Fernwärme, in fast allen Effizienzklassen vertreten sind.

In [36]:
#Ändert sich die Art des Hauses je nach Entfernung zum Stadtzentrum?
fig = px.density_heatmap(df, x="Art", y="MaxOrt",
                         marginal_x="histogram", marginal_y="histogram")
fig.show("notebook")

Es gibt keine Villen, Bungalows und Bauernhäuser innerhalb 7 km zum Stadtzentrum. Zwischen 8 und 22 km vom Zentrum gibt es sehr viele Einfamilien-, Mehrfamilien- udn Reihenhäuser sowie Doppelhaushälften, all deren Anzahl nimmt mit größerem Abstand jedoch ab.

3. Regression¶

Im Regressionskapitel werden die vorhandenen Daten in ein verwendbares Format gebracht, sowie in ein Test- und Trainingsset unterteilt. Damit werden dann verschiedene Regressionsmodelle erstellt, optimiert und mit einander verglichen. Ziel ist es bestmöglich den Hauspreis vorherzusagen.

3.1. Preprocessing¶

Im Preprocessing werden die Daten in ein verwendbares Format für die Regression gebracht. Das beinhaltet zum einen das One-Hot-Encoding kategorialer Features, zu anderen auch den split in ein Trainings- und Testset.

3.1.1. One-Hot-Encoding¶

Beim One-Hot-Encoding wird jeder kategoriale Wert in eine eigene Spalte geschrieben und mit 1 = wahr oder 0 = falsch versehen. Damit werden alle kategorialen Features in Numerische umgewandelt und können damit besser verarbeitet werden in der Regression.

In [37]:
cat_features
Out[37]:
['ID', 'Ort', 'Stand', 'Art', 'Effizienz', 'Energietraeger', 'Heizung']
In [38]:
df.shape
Out[38]:
(8166, 14)
In [39]:
#One Hot Encoding kategorialer Features
dfOH=pd.get_dummies(df,columns=["Ort", "Art", "Effizienz", "Energietraeger", "Heizung"])
dfOH = dfOH.drop('ID', axis=1) #hilft nicht bei Regression
dfOH = dfOH.drop('Stand', axis=1) #hilft nicht bei Regression
pr=dfOH["Preis"] #ab hier: Preis als lettze Spalte setzen
dfOH.drop(labels=['Preis'], axis=1, inplace = True) 
dfOH.insert(len(dfOH.columns), 'Preis', pr)
dfOH.head()
Out[39]:
MaxOrt Flaeche Raeume Grundstuecksflaeche Geschosse Jahr Ort_berlin Ort_frankfurt-am-main Ort_hamburg Ort_koeln ... Heizung_Luft-/Wasser-Wärmepumpe, offener Kamin Heizung_Luft-/Wasser-Wärmepumpe, offener Kamin, Zentralheizung Heizung_Ofen Heizung_Ofen, Zentralheizung Heizung_Ofen, offener Kamin Heizung_Ofen, offener Kamin, Zentralheizung Heizung_Zentralheizung Heizung_offener Kamin Heizung_offener Kamin, Zentralheizung Preis
0 15.0 780.0 30.0 1696.0 5.000000 1890.0 1 0 0 0 ... 0 0 0 0 0 0 1 0 0 7790000
1 30.0 111.0 5.0 1100.0 2.000000 1936.0 1 0 0 0 ... 0 0 0 0 0 0 1 0 0 450000
2 10.0 323.0 8.0 393.0 2.417838 1990.0 1 0 0 0 ... 0 0 0 0 0 0 1 0 0 690000
3 10.0 97.0 4.0 407.0 2.417838 1958.0 1 0 0 0 ... 0 0 0 0 0 0 1 0 0 429000
4 10.0 18662.0 8.0 502.0 2.417838 1936.0 1 0 0 0 ... 0 0 0 0 0 0 1 0 0 1700000

5 rows × 132 columns

3.1.2. Test-Train-Split¶

Das Dataframe wird nun in zwei Teile gesplittet: Test- und Trainingsdaten. Mit den Trainingsdaten wird das Modell trainiert und es lernt die Testdaten nicht kennen. Die Vorhersagen des Modells aus den Trainingsdaten werden dann mit den Trainingsdaten abgegelichen, um zu schauen wie gut die Performance tatsächlich mit neuen Daten ist. Innerhalb dieser Sets wird wiederum in X und y unterschieden: X sind die Features, durch die das Modell lernt und y ist der Wert, der vorhergesagt werden soll (hier: Preis).

Die Standardisierung der numerischen Features dient dazu alle numerischen Features auf die selbe Skalierung zu bringen, sodass große Zahlen keinen größeren Einfluss auf das Modell haben, sondern deren Verhältnis ist das Wichtige.

In [436]:
#Split X y
X=dfOH.iloc[:,:-1] 
y=dfOH.iloc[:,-1]

#Standardisierung von X
scaler = MinMaxScaler() #getestet: StandardScaler, MinMaxScaler (ein Einfluss auf R2)
X=pd.DataFrame(scaler.fit_transform(X), columns=X.columns)

#Split Test Train
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3,random_state=0)
In [437]:
print(X_train.shape)
print(X_test.shape)
print(y_train.shape)
print(y_test.shape)
(5716, 131)
(2450, 131)
(5716,)
(2450,)

Durch den Split und das Preprocessing haben wir nun unser Trainingsset mit 5716 Einträgen, also 70%, und das Testset mit den anderen 30%, also 2450 Zeilen. Die Anzahl der Spalten von X hat sich durch das One-Hot-Encoding enorm erhöht, von 12 auf 131.

3.2. Regressionsmodelle¶

Mit den enstandenen Datensets werden nun verschiedene Regressionsmodelle erstellt, um den Hauspreis vorherzusagen. Dafür wird erst die Library statsmodel für Ordinary Least Squares (OLS = Lineare Regression) verwendet und danach sklearn für Pipelines, wo mit Crossvalidierung mehrere Modelle verglichen und optimiert werden.

3.2.1. Statsmodel¶

Zuerst wird ein OLS Modell von Preis zu einem Feature erstellt. Hierbei wird Flaeche gewählt, da es die höchste Korrelation aufweist.

In [438]:
#statsmodel mit einem Feature
lm = smf.ols(formula ='Preis ~ Flaeche', data=dfOH).fit()
#Regression results
lm.summary()
Out[438]:
OLS Regression Results
Dep. Variable: Preis R-squared: 0.038
Model: OLS Adj. R-squared: 0.038
Method: Least Squares F-statistic: 319.4
Date: Fri, 06 Jan 2023 Prob (F-statistic): 4.23e-70
Time: 13:01:11 Log-Likelihood: -1.2700e+05
No. Observations: 8166 AIC: 2.540e+05
Df Residuals: 8164 BIC: 2.540e+05
Df Model: 1
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
Intercept 9.682e+05 1.59e+04 60.710 0.000 9.37e+05 9.99e+05
Flaeche 25.3055 1.416 17.871 0.000 22.530 28.081
Omnibus: 11010.814 Durbin-Watson: 1.517
Prob(Omnibus): 0.000 Jarque-Bera (JB): 3139703.091
Skew: 7.653 Prob(JB): 0.00
Kurtosis: 97.833 Cond. No. 1.18e+04


Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.18e+04. This might indicate that there are
strong multicollinearity or other numerical problems.

Ergebnisse OLS mit einem Feature:

  • R squared gibt die Verbesserung der Vorhersage durch das Modell an (Optimum: 1) --> 0,038 ist sehr schlecht
  • Adjusted R squared bezieht die Freiheitsgrade in R2 ein --> 0,038 ist ebenfalls sehr schlecht
  • F-Statistic beschreibt wie das Modell die Vorhersage verbessert hat im Vergleich zum Level der Ungenauigkeit im Modell (Optimum: > 1) --> 319 > 1, also gut
  • Hypothese H0 wird verworfen, da t groß ist P>t = 0 --> Effekt von Flaeche auf Preis ist bedeutend
  • Keine Normalverteilung der Errors, da Omnibus groß ist (normal verteilt: Omnibus = 0) und Prob(Omnibus) = 0 (Optimum: 1)
  • Daten sind assymmetrisch, da Skew groß ist (bei Symmetrie: Skew = 0)
  • Kurtosis beschreibt die Krümmung der Daten (Optimum: hoch) --> 97 bedeutet krumme Daten

Nun werden Ausreißer betrachtet, denn in der EDA wurden bereits einige Ausreißer identifiziert, aber nicht beseitigt. Diese können nun die Modellperformance beinflusssen. Als Methode wird Cook's Distance verwendet.

In [439]:
#Cook's distance 
lm_cooksd = lm.get_influence().cooks_distance[0]

#Länge von dfOH für n
n = len(dfOH['Preis'])

#Critical d
critical_d = 4/n
print('Critical Cooks distance:', critical_d)

#Identifikation möglicher Ausreißer mit Leverage
out_d = lm_cooksd > critical_d

#Ausreißer entfernen
oultiers = dfOH.index[out_d]
subset = ~dfOH.index.isin(['outliers'])
Critical Cooks distance: 0.0004898359049718344

Nun wird das OLS mit einem Feature, Flaeche, erneut durchgeführt, jedoch werden die Ausreißer über das subset entfernt.

In [440]:
#statsmodel mit einem Feature und ohne Ausreißer
lm2 = ols("Preis ~ Flaeche", data=dfOH, subset=subset).fit()

print(lm2.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                  Preis   R-squared:                       0.038
Model:                            OLS   Adj. R-squared:                  0.038
Method:                 Least Squares   F-statistic:                     319.4
Date:                Fri, 06 Jan 2023   Prob (F-statistic):           4.23e-70
Time:                        13:01:17   Log-Likelihood:            -1.2700e+05
No. Observations:                8166   AIC:                         2.540e+05
Df Residuals:                    8164   BIC:                         2.540e+05
Df Model:                           1                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
Intercept   9.682e+05   1.59e+04     60.710      0.000    9.37e+05    9.99e+05
Flaeche       25.3055      1.416     17.871      0.000      22.530      28.081
==============================================================================
Omnibus:                    11010.814   Durbin-Watson:                   1.517
Prob(Omnibus):                  0.000   Jarque-Bera (JB):          3139703.091
Skew:                           7.653   Prob(JB):                         0.00
Kurtosis:                      97.833   Cond. No.                     1.18e+04
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.18e+04. This might indicate that there are
strong multicollinearity or other numerical problems.

Ergebnisse OLS mit einem Feature und ohne Ausreißer:

Keine Performanceverbesserung bzw. Veränderung zu mit Ausreißern zu verzeichnen.

Für ein OLS Modell mit mehreren Features, wird zuvor die Wichtigkeit der Features untersucht. Je größer der Balken im nachfolgenden Plot, desto größer die Wichtigkeit. Aus den wichtigsten Features wird dann ein neues OLS Modell gebaut.

In [441]:
#Feature importance
reg = LassoCV(cv=5, random_state=10, max_iter=10000).fit(X_train, y_train) 

#Absolute Werte der Koeffizienten
importance = np.abs(reg.coef_)
feature_names = X_train.columns

sns.set(font_scale = 0.4) #Plot mit Zoom-Funktion sichten oder Versuch Features = 0 nicht anzeigen
sns.barplot(x=importance, 
            y=feature_names)
Out[441]:
<AxesSubplot:>

Da sich der Feature Importance Plot kaum lesen lässt, wird auf eine alternative Feature Selection Mthode, Forward Selection zurückgegriffen.

In [442]:
tic_fwd = time()

sfs_forward = SequentialFeatureSelector(
    reg, n_features_to_select=5, 
    direction="forward").fit(X_train, y_train)

toc_fwd = time()

print(
    "Features selected by forward sequential selection: "
    f"{feature_names[sfs_forward.get_support()]}"
)
print(f"Done in {toc_fwd - tic_fwd:.3f}s")
Features selected by forward sequential selection: Index(['MaxOrt', 'Flaeche', 'Ort_muenchen', 'Art_Mehrfamilienhaus',
       'Art_Villa'],
      dtype='object')
Done in 84.240s
In [443]:
lm3 = ols("Preis ~ MaxOrt + Flaeche + Ort_muenchen + Art_Mehrfamilienhaus + Art_Villa", data=dfOH).fit()

print(lm3.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                  Preis   R-squared:                       0.191
Model:                            OLS   Adj. R-squared:                  0.191
Method:                 Least Squares   F-statistic:                     385.6
Date:                Fri, 06 Jan 2023   Prob (F-statistic):               0.00
Time:                        13:02:48   Log-Likelihood:            -1.2629e+05
No. Observations:                8166   AIC:                         2.526e+05
Df Residuals:                    8160   BIC:                         2.526e+05
Df Model:                           5                                         
Covariance Type:            nonrobust                                         
========================================================================================
                           coef    std err          t      P>|t|      [0.025      0.975]
----------------------------------------------------------------------------------------
Intercept             1.121e+06   2.84e+04     39.411      0.000    1.06e+06    1.18e+06
MaxOrt               -2.124e+04   1072.608    -19.802      0.000   -2.33e+04   -1.91e+04
Flaeche                 19.3178      1.314     14.707      0.000      16.743      21.893
Ort_muenchen          9.922e+05   4.02e+04     24.681      0.000    9.13e+05    1.07e+06
Art_Mehrfamilienhaus  5.344e+05   3.49e+04     15.313      0.000    4.66e+05    6.03e+05
Art_Villa             1.599e+06   7.98e+04     20.029      0.000    1.44e+06    1.76e+06
==============================================================================
Omnibus:                    11287.361   Durbin-Watson:                   1.691
Prob(Omnibus):                  0.000   Jarque-Bera (JB):          4109440.829
Skew:                           7.926   Prob(JB):                         0.00
Kurtosis:                     111.749   Cond. No.                     6.47e+04
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 6.47e+04. This might indicate that there are
strong multicollinearity or other numerical problems.

Ergebnisse OLS mit den wichtigsten Features:

  • R squared gibt die Verbesserung der Vorhersage durch das Modell an (Optimum: 1) --> 0,19 ist nicht wirklich gut
  • Adjusted R squared bezieht die Freiheitsgrade in R2 ein --> 0,19 ist auch nicht gut
  • F-Statistic beschreibt wie das Modell die Vorhersage verbessert hat im Vergleich zum Level der Ungenauigkeit im Modell (Optimum: > 1) --> 385 > 1, also gut
  • Keine Normalverteilung der Errors, da Omnibus groß ist (normal verteilt: Omnibus = 0) und Prob(Omnibus) = 0 (Optimum: 1)
  • Daten sind assymmetrisch, da Skew groß ist (bei Symmetrie: Skew = 0)

Nun wird überprüft, ob Multikollinearität zwischen den verwendeten Features vorliegt. Multikollinearität beschreibt die Korrelation von Features untereinander, wenn der Variance Inflation Factor (VIF) > 5. Diese muss entfernt werden, da das Modell sonst zu Overfitting tendiert.

In [444]:
#Multicollinearity
y, X = dmatrices('Preis ~  MaxOrt + Flaeche + Ort_muenchen + Art_Mehrfamilienhaus + Art_Villa', dfOH, return_type='dataframe')

#Für jedes X wird der VIF berechnet und in ein df gespeichert
vif = pd.DataFrame()
vif["VIF Factor"] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
vif["Feature"] = X.columns

vif.round(2)
Out[444]:
VIF Factor Feature
0 4.16 Intercept
1 1.01 MaxOrt
2 1.02 Flaeche
3 1.01 Ort_muenchen
4 1.03 Art_Mehrfamilienhaus
5 1.01 Art_Villa

Der VIF zwischen den Features liegt immer unterhalb von 5, daher liegt keine kritische Multikollinearität im Modell vor.

Das OLS Modell mit den wichtigsten Features hat bei statsmodel die beste Performance erzielt, mit einem R2 von 0,19.

3.2.2. Sklearn¶

Mit sklearn werden weitere Regressionsmodelle verwendet, optimiert und verglichen. Dafür werden alle Parameter optimeriert. Die betrachteten Modelle sind:

  • Linear Regression: Minimiert die Quadratsumme zwischen den Zielwerten im Datensatz und den Vorhersagen durch die lineare Annäherung
  • Bayesian Ridge: Lineare Annäherung mit Wahrscheinlichkeitsvertreilung statt der Vorhersage einzelner Punkte
  • Support Vector Regression: Zieht eine passende Hyperplane/Linie durch die Daten
  • Stochastic Gradient Descent Regressor: Der Error jedes Punktes wird nacheinander berechnet und das Modell durch die neuen Erkenntnisse verbessert.
  • Decision Tree Regressor: Der Wert wird durch das Herunterbrechen der Featureausprägungen in Äste verfeinert, dadurch entsteht eine Baumstruktur.
In [445]:
#Pipelines für mehrere Methoden der Regression

#Linear Regression
pipe1 = Pipeline([('scaler', StandardScaler()),
                    ('lr', LinearRegression(fit_intercept=True, #Nicht zentrierte Daten, daher intercept=Ture benötigt
                                            copy_X=True, #getestet: True, False (komisches Ergebnis bei False)
                                            n_jobs=None, #nicht benötigt, da Problem nicht so groß
                                            positive=False))]) #True nur bei dense arryas möglich, hier nicht

#Bayesian Ridge
pipe2 = Pipeline([('scaler', StandardScaler()),
                    ('br', BayesianRidge(n_iter=300, #getestet: 300, 50, 500 (kein Einfluss auf R2)
                                        tol=0.001, #getestet: 0.001, 0.1, 0.0001 (0.1 komisches Ergebnis, andere Werte keinen Einfluss)
                                        alpha_1=1e-06, #getestet: 1e-06, 1e-02, 1e-010 (kein Eifnluss auf R2)
                                        alpha_2=1e-06, #getestet: 1e-06, 1e-02, 1e-010 (kein Einfluss auf R2)
                                        lambda_1=1e-06, #getestet: 1e-06, 1e-02, 1e-010 (winziges Ergebnis bei 1e-02, kein Einfluss anderer Werte auf R2)
                                        lambda_2=1e-06, #getestet: 1e-06, 1e-02, 1e-010 (kein Einfluss auf R2)
                                        alpha_init=None, #getestet: None, 0.1, 1 (kein Einfluss auf R2)
                                        lambda_init=None, #getestet: None, 0.1, 2 (kein Einfluss auf R2)
                                        compute_score=False, #getestes: False, True (kein Einfluss auf R2)
                                        fit_intercept=True, #Nicht zentrierte Daten, daher intercept=Ture benötigt
                                        copy_X=True, #getestet: True, False (kein Einfluss auf R2)
                                        verbose=False))]) #getestet: False, True (kein Einfluss auf R2)

#Support Vector Regression
#keinen positiven R2 erreicht
pipe3 = Pipeline([('scaler', StandardScaler()),
                    ('svr', SVR(kernel='rbf', #getestet: linear, poly, rbf, sigmoid, precomputed, callable (negatives Ergebnis bei rbf, linear, poly und sigmoid, Error bei precomputed und callable)
                                degree=10, #nur für poly, getestet: 3, 5, 10 (bestes Ergebnis, aber mit Meldung für preprocessing, owbohl diese gemacht wird)
                                gamma='scale', #getestet: scale, auto, 1.0 (kein Einfluss auf R2)
                                coef0=0.0, #nur für poly und sigmoig, getestet: 0.0, 1.0
                                tol=0.0001, #getestet: 0.001, 0.1, 0.0001 (kein Einfluss auf R2)
                                C=1.0, #getestet: 1.0, 0.1 (kein Einfluss auf R2)
                                epsilon=0.1, #getestet: 0.1, 1.0 (kein Einfluss auf R2)
                                shrinking=True, #getestet: True, False (kein Einfluss auf R2)
                                cache_size=200, #keep 100
                                verbose=False, #getestet: False, True (kein Einfluss auf R2)
                                max_iter=10000))]) #getestet -1, 10, 1000, 10000 (nur bei 10000 und -1 keine Meldung, dass max_iter erhöht werden soll)

#Stochastic Gradient Descent Regressor
#keinen positiven R2 erreicht
pipe4 = Pipeline([('scaler', StandardScaler()),
                    ('sgd', SGDRegressor(loss='huber', #getestet: squared_error, huber, epsilon_insensitive, squared_epsilon_insensitive (negatives Ergebnis bei squared_error, huber, epsilon_insensitive, squared_epsilon_insensitive)
                                        penalty='elasticnet', #getestet: l2, l1, elasticnet, None (kein Einfluss auf R2)
                                        alpha=0.1, #getestet: 0.0001, 0.1 (kein Einfluss auf R2)
                                        l1_ratio=0.15, #nur für elasticnet, getestet: 0.15, 0.5, 0.75 (kein Einfluss auf R2)
                                        fit_intercept=True, #Nicht zentrierte Daten, daher intercept=Ture benötigt
                                        max_iter=10000, #getestet: 1000, 10000
                                        tol=0.001, #getestet: 0.001, 0.1, None (None verlängert Laufzeit enorm, bei anderen Werten kein Eifnluss auf R2)
                                        shuffle=True, #getestet: True, False (kein Einfluss auf R2)
                                        verbose=0, #getestet: 0, 1, 10 (kein Einfluss auf R2)
                                        epsilon=1.0, #getestet: 0.1, 1.0 (kein Einfluss auf R2)
                                        random_state=None, #keep None
                                        learning_rate='adaptive', #getestet: invscaling, constant, optimal, adaptive (kein Einfluss auf R2)
                                        eta0=0.01, #getestet: 0.01, 0.1 (kein Einfluss auf R2)
                                        power_t=0.25, #nur für invscaling, getestet: 0.25, 1.0 (kein Einfluss auf R2)
                                        early_stopping=True, #getestet: False, True (True verkürzt Laufzeit enorm mit nur minimal schlechterem Ergebnis)
                                        validation_fraction=0.1, #nur für earla_stopping=True, getestet: 0.1, 0.5 (kein Einfluss auf R2)
                                        n_iter_no_change=5, #getestet: 5, 50 (kein Einfluss auf R2)
                                        warm_start=False, #getestet: False, True (kein Einfluss auf R2)
                                        average=False))]) #getestet: False, True 

#Decision Tree Regressor
pipe5 = Pipeline([('scaler', StandardScaler()),
                    ('dtr', DecisionTreeRegressor(criterion='poisson', #getestet: squared_error, friedman_mse, absolute_error, poisson
                                                splitter='best', #getestet: best, random
                                                max_depth=5, #getestet: None, 5, 10 (nur mit 5 positives Ergebnis bei allen Varianten für R2)
                                                min_samples_split=2, #getestet: 2, 5, 10 (kein Einfluss auf R2)
                                                min_samples_leaf=10, #getestet: 1, 3, 10
                                                min_weight_fraction_leaf=0.0, #getestet: 0.0, 0.1, 0.3
                                                max_features=None, #getestet: None, 5, 10
                                                random_state=None, #keep None
                                                max_leaf_nodes=None, #getestet: None, 5, 10
                                                min_impurity_decrease=1.0, #getestet: 0.0, 0.3, 1.0 (kein Einfluss auf R2)
                                                ccp_alpha=0.0))]) #getestet: 0.0, 0.3, 1.0 (kein Einfluss auf R2)

Für die Modelle wird nun eine 10-Fold Crossvalisierung durchgeführt und die durchschnittlichen R2-Werte danach ausgegeben.

In [446]:
# Cross validation jeder Pipeline
scores = [cross_val_score(mypipe, X_train, y_train, scoring='r2', cv=10) 
            for mypipe in [pipe1,pipe2,pipe3,pipe4,pipe5]]
In [447]:
#Ausgabe der Ergebnisse
for score,label in zip(scores, 
                       ['Linear Regression', 
                        'Bayesian Ridge',
                        'Support Vector Regression',
                        'Stochastic Gradient Descent Regressor',
                        'Decision Tree Regressor'
                        ]
                       ):
    print("R2: {:.2} (+/- {:.2}), {:}".format(score.mean(), score.std(), label))
R2: -1.5e+23 (+/- 1.3e+23), Linear Regression
R2: 0.24 (+/- 0.054), Bayesian Ridge
R2: -0.064 (+/- 0.016), Support Vector Regression
R2: -0.68 (+/- 0.2), Stochastic Gradient Descent Regressor
R2: 0.29 (+/- 0.088), Decision Tree Regressor

Ergebnisse:

Der R2 Wert ist das Bestimmtheitsmaß und sollte zwischen 0 und 1 liegen. Es ist komisch, dass hier negative Werte herauskommen. Die einzigen realistischen Ergebnisse sind bei Bayesian Ridge und dem Decision Tree Regressor. Der Decision Tree hat mit einem R2 von 0.29 die beste PErformance.

Da wir den Fehler in der Crossvalidierung nicht finden konnten, berechnen wir nun die Werte manuell mit einzelner Crossvalidierung.

In [448]:
#Vergleich Vorhersage zu tatsächlichem Hauspreis
pipe1.fit(X_train, y_train)
pipe2.fit(X_train, y_train)
pipe3.fit(X_train, y_train)
pipe4.fit(X_train, y_train)
pipe5.fit(X_train, y_train)
Out[448]:
Pipeline(steps=[('scaler', StandardScaler()),
                ('dtr',
                 DecisionTreeRegressor(criterion='poisson', max_depth=5,
                                       min_impurity_decrease=1.0,
                                       min_samples_leaf=10))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('scaler', StandardScaler()),
                ('dtr',
                 DecisionTreeRegressor(criterion='poisson', max_depth=5,
                                       min_impurity_decrease=1.0,
                                       min_samples_leaf=10))])
StandardScaler()
DecisionTreeRegressor(criterion='poisson', max_depth=5,
                      min_impurity_decrease=1.0, min_samples_leaf=10)
In [450]:
#cross validation score
scores_1 = cross_val_score(pipe1, X_train, y_train, scoring = 'r2', cv = 10)
scores_2 = cross_val_score(pipe2, X_train, y_train, scoring = 'r2', cv = 10)
scores_3 = cross_val_score(pipe3, X_train, y_train, scoring = 'r2', cv = 10)
scores_4 = cross_val_score(pipe4, X_train, y_train, scoring = 'r2', cv = 10)
scores_5 = cross_val_score(pipe5, X_train, y_train, scoring = 'r2', cv = 10)

print("mean cross validation score Linear Regression: {:.2}".format(np.mean(scores_1)))
print("mean cross validation score Bayesian Ridge: {:.2}".format(np.mean(scores_2)))
print("mean cross validation score Support Vector Machine: {:.2}".format(np.mean(scores_3)))
print("mean cross validation score Stochastic Gradient Descent Regressor: {:.2}".format(np.mean(scores_4)))
print("mean cross validation score Decision Tree Regressor: {:.2}".format(np.mean(scores_5)))
mean cross validation score Linear Regression: -1.5e+23
mean cross validation score Bayesian Ridge: 0.24
mean cross validation score Support Vector Machine: -0.064
mean cross validation score Stochastic Gradient Descent Regressor: -0.68
mean cross validation score Decision Tree Regressor: 0.29

Die manuelle Crossvalidierung ergibt die selben Ergebnisse. Nun wird noch versucht mit y_pred und r2_score() ein zuverlässigeren Ergebnis zu erzielen.

In [449]:
#Werte, die das Modell vorhersagt
y_pred_1 = pipe1.predict(X_train)
y_pred_2 = pipe2.predict(X_train)
y_pred_3 = pipe3.predict(X_train)
y_pred_4 = pipe4.predict(X_train)
y_pred_5 = pipe5.predict(X_train)
In [451]:
#R2 aus tatsächlichen vs. vorhergesagten Werten
R2_1 = r2_score(y_train, y_pred_1)
R2_2 = r2_score(y_train, y_pred_2)
R2_3 = r2_score(y_train, y_pred_3)
R2_4 = r2_score(y_train, y_pred_4)
R2_5 = r2_score(y_train, y_pred_5)

print("R2 Linear Regression:", R2_1.round(2))
print("R2 Bayesian Ridge:", R2_2.round(2))
print("R2 Support Vector Machine:", R2_3.round(2))
print("R2 Stochastic Gradient Descent Regressor:", R2_4.round(2))
print("R2 Decision Tree Regressor:", R2_5.round(2))
R2 Linear Regression: 0.26
R2 Bayesian Ridge: 0.26
R2 Support Vector Machine: -0.06
R2 Stochastic Gradient Descent Regressor: -0.6
R2 Decision Tree Regressor: 0.43

Ergebnisse:

Hiermit sind die unterschiede etwas anders und nur noch 2 der 5 Modelle zeigen einen negativen R2-Wert. Den besten R2 hat mit 0.43 auch hier der Decision Tree Regressor. Dies ist also das Modell, welches den Hauspreis am besten vorhersagt, auch besser als die Lineare Regression mit statsmodel.

4. Inventardauer der Inserate¶

Es wird untersucht, wie lange die Inserate im Durchschnitt online sind. Dies gibt einen Aufschluss darüber, wie lange es dauert bis Häuser verkauft bzw. ausreichend Interessenten für den Kauf vorhanden sind.

In [56]:
#Zählen wie häufig die IDs vorkommen, jedes Vorkommen ist ein Tag
inv = immodf.groupby(['ID']).size().reset_index(name='count')
inv = inv[~inv['count'].isin([2])]

inv.head()
Out[56]:
ID count
0 222f85z 8
1 222mb5l 8
2 223j85z 8
3 223m85z 8
4 224g85z 8
In [57]:
#Ausprägung von count
inv.describe().T
Out[57]:
count mean std min 25% 50% 75% max
count 7656.0 7.108934 1.792325 1.0 7.0 8.0 8.0 11.0

Die kürzeste Inventardauer beträgt einen Tag und die höchste 8 Tage. Im Durchschnitt sind die Häuser 7 Tage lang inseriert.

Bei einer längeren Betrachtungsdauer würde sich ein zuverlässigerer Wert der Inventardauer herauskristallisieren. Da auch immer nur 50 Seiten statt aller betrachtet werden, kann es sein, dass eineige Häuser auch nur auf Seite 51 oder folgende gerutscht sind, aber eigentlich noch inseriert sind.

Unterscheidet sich die Inventardauer je Stadt?

In [58]:
#Durchschnittliche Inventardauer pro Ort

inv = immodf.groupby(['ID','Ort']).size().reset_index(name='count')
inv = inv[~inv['count'].isin([20])]
In [59]:
inv.groupby(by="Ort").describe().applymap('{:,.2f}'.format)
Out[59]:
count
count mean std min 25% 50% 75% max
Ort
berlin 1,189.00 6.73 2.23 1.00 6.00 8.00 8.00 11.00
frankfurt-am-main 1,143.00 6.98 2.04 1.00 7.00 8.00 8.00 9.00
hamburg 1,170.00 6.84 2.19 1.00 7.00 8.00 8.00 8.00
koeln 1,133.00 7.06 1.97 1.00 8.00 8.00 8.00 9.00
leipzig 1,117.00 7.16 1.93 1.00 8.00 8.00 8.00 9.00
muenchen 1,096.00 7.30 1.80 1.00 8.00 8.00 8.00 8.00
stuttgart 1,115.00 6.33 1.70 1.00 7.00 7.00 7.00 8.00

Die durchschnittlich längste Inventardauer haben Exposes in Leipzig, am kürzesten in Stuttgart. Die Werte liegen jedoch alle sehr nah bei einander, bei 6,33 bis 7,3 Tagen.

5. Verbindung mit Bauzinsen¶

In diesem letzten Kapitel werden die täglich eingelesenen Bauzinsen mit den Hauspreisen in Verbidnung gebracht. Dabei wird untersucht, ob die Zinshöhe den Hauspreis beeinflusst.

Zuerst werden die täglich gescrapten Zinsen eingelesen und danach in ein gemeinsames dataframe gemerget. Das Vorgehen ist dasselbe wie beim Einlesen der Immoboliendateien.

In [60]:
#Upload
zins2812=pd.read_csv("daily_data//zins//2022-12-28_Zinsen.csv")
zins2912=pd.read_csv("daily_data//zins//2022-12-29_Zinsen.csv")
zins3012=pd.read_csv("daily_data//zins//2022-12-30_Zinsen.csv")
zins3112=pd.read_csv("daily_data//zins//2022-12-31_Zinsen.csv")
zins0101=pd.read_csv("daily_data//zins//2023-01-01_Zinsen.csv")
zins0201=pd.read_csv("daily_data//zins//2023-01-02_Zinsen.csv")
zins0301=pd.read_csv("daily_data//zins//2023-01-03_Zinsen.csv")
zins0401=pd.read_csv("daily_data//zins//2023-01-04_Zinsen.csv")
zins0501=pd.read_csv("daily_data//zins//2023-01-05_Zinsen.csv")
zins2912
Out[60]:
Unnamed: 0 Sollzinsbindung Effektiver Jahreszins Datenstand
0 0 5 Jahre ['3,53'] ['28.12.2022 16:30']
1 1 10 Jahre ['3,51'] ['28.12.2022 16:30']
2 2 15 Jahre ['3,64'] ['28.12.2022 16:30']
3 3 20 Jahre ['3,79'] ['28.12.2022 16:30']
4 4 25 Jahre ['3,86'] ['28.12.2022 16:30']
In [61]:
#Merge
zinsdf = pd.concat([zins2812,zins2912,zins3012,zins3112,zins0101,zins0201,zins0301,zins0401,zins0501],ignore_index=True)
print(zinsdf.shape)
zinsdf.head(10)
(45, 4)
Out[61]:
Unnamed: 0 Sollzinsbindung Effektiver Jahreszins Datenstand
0 0 5 Jahre ['3,53'] ['28.12.2022 11:30']
1 1 10 Jahre ['3,51'] ['28.12.2022 11:30']
2 2 15 Jahre ['3,64'] ['28.12.2022 11:30']
3 3 20 Jahre ['3,79'] ['28.12.2022 11:30']
4 4 25 Jahre ['3,86'] ['28.12.2022 11:30']
5 0 5 Jahre ['3,53'] ['28.12.2022 16:30']
6 1 10 Jahre ['3,51'] ['28.12.2022 16:30']
7 2 15 Jahre ['3,64'] ['28.12.2022 16:30']
8 3 20 Jahre ['3,79'] ['28.12.2022 16:30']
9 4 25 Jahre ['3,86'] ['28.12.2022 16:30']

Auch im zinsdf sind noch einige ungewollte Sonderzeichen und Worte inbegriffen. Diese werden im nächsten Schritt entfernt.

In [62]:
#data prep
zinsdf['Sollzinsbindung'] = zinsdf['Sollzinsbindung'].str.replace(" Jahre","")
zinsdf.loc[:,"Sollzinsbindung"]=pd.to_numeric(zinsdf["Sollzinsbindung"])

zinsdf['Jahreszins'] = zinsdf['Effektiver Jahreszins'].apply(lambda x: x[1:-1])
zinsdf['Jahreszins'] = zinsdf['Jahreszins'].str.replace(",",".")
zinsdf['Jahreszins'] = zinsdf['Jahreszins'].str.replace("'","")
zinsdf.loc[:,"Jahreszins"]=pd.to_numeric(zinsdf["Jahreszins"])
zinsdf = zinsdf.drop('Effektiver Jahreszins', axis=1)

zinsdf['Stand'] = zinsdf['Datenstand'].apply(lambda x: x[1:-1])
zinsdf['Stand'] = zinsdf['Stand'].str.replace(",",".")
zinsdf['Stand'] = zinsdf['Stand'].str.replace("'","")
zinsdf['Stand'] = zinsdf['Stand'].str.replace("28.12.2022 16:30","29.12.2022 16:30")
#zinsdf['Stand'] = pd.to_datetime(zinsdf['Stand'], format='%d%m%Y:%H:%M:%S') #Zeitformat stimmt noch nicht
zinsdf = zinsdf.drop('Datenstand', axis=1)

zinsdf = zinsdf.drop('Unnamed: 0', axis=1)
/var/folders/1c/xbr5tqd11kj2_4xtc05ft2dc0000gp/T/ipykernel_51481/1162883209.py:14: FutureWarning:

The default value of regex will change from True to False in a future version.

In [63]:
zinsdf.head(10)
Out[63]:
Sollzinsbindung Jahreszins Stand
0 5 3.53 28.12.2022 11:30
1 10 3.51 28.12.2022 11:30
2 15 3.64 28.12.2022 11:30
3 20 3.79 28.12.2022 11:30
4 25 3.86 28.12.2022 11:30
5 5 3.53 29.12.2022 16:30
6 10 3.51 29.12.2022 16:30
7 15 3.64 29.12.2022 16:30
8 20 3.79 29.12.2022 16:30
9 25 3.86 29.12.2022 16:30

Das zinsdf ist nun zur weiteren Verwendung bereit.

5.1. Verlauf Hauspreise zu Bauzinsen¶

Es wird untersucht, ob die Zinshöhe die Hauspreise beeinflusst. Die Vermutung ist, dass höhere Bauzinsen etwas niedrigere Hauspreise bedeuten.

Zuerst werden die Bauzinsen und Hauspreise einzeln visuell dargestellt und danach in kombiniert.

In [64]:
#Zinshöhe
sns.pointplot(data=zinsdf, x='Stand', y='Jahreszins', hue='Sollzinsbindung')
Out[64]:
<AxesSubplot:xlabel='Stand', ylabel='Jahreszins'>

Die Zinshöhe bei einer Sollzinsbindung von 5 und 25 Jahren stagniert ziemlich. Die Bauzinsen bei einer Sollzinsbindung von 20 Jahren fallen leicht, hingegen bei einer Sollzinsbindung von 10 und 15 Jahren steigen sie im betrachteten Zeitraum zuerst und fallen am letzten Tag enorm.

In [65]:
#Hauspreise
sns.pointplot(data=df, x='Stand', y='Preis')
Out[65]:
<AxesSubplot:xlabel='Stand', ylabel='Preis'>

Die Hauspreise der neu inserierten Häuser fallen vom 28. auf 29.12.2022, steigen danach jedoch wieder auf den ursprünglichen Durchschnittswert.

In [66]:
#Bauzinsen und Hauspreise kombiniert
fig,(ax1, ax2)= plt.subplots(nrows=2)
fig.set_size_inches(18, 14)

sns.pointplot(data=zinsdf, x='Stand', y='Jahreszins', hue='Sollzinsbindung', ax=ax1)
sns.pointplot(data=df, x='Stand', y='Preis', ax=ax2)
Out[66]:
<AxesSubplot:xlabel='Stand', ylabel='Preis'>

Im direkten Vergleich lässt sich keine ähnliche oder entgegensetzte Struktur der Verläufe von Hauspreisen und Zinsen ablesen. Der betrachtete Zeitraum ist jedoch mit nur wenigen Tagen sehr kurz. Bei einer Fortführung über längeren Zeitraum ist es möglich, dass einen Zusammenhang ablesbar werden könnte. Dann könnte außerdem mit einem neuronalen Netz eine Zeitreihenvorhersage für Hauspreise und Bauzinssätze durchgeführt werden.

6. Fazit¶

Für das Projekt wurden zuerst die inserierten Häuser von Immowelt aus sieben deutschen Städten gescrapet. Bis zum letztendlich verwendeten Code wurden einige Schritte mit xpath durchgeführt: Zuerst wurde eine Seite eines Ortes gescrapet, dann alle Seiten eines Ortes, außerdem wurde in alle Exposes gegangen um weitere Eigenschaften der Häuser zu inkludieren, und zuletzt wurde das für alle Orte durchgeführt. Am Ende sind alle Schritte in einer großen For-Schleife vereint und werden von einer Matrix in ein Pandas Dataframe umgewandelt. Dabei war es von Vorteil, dass die URL sehr logisch aufgebaut ist und sich einzelne Bausteine ändern lassen, um so auf eine andere Seite und anderen Ort zu wechseln. Das selbe gilt für die URL der Exposes. Kompliziert wurde Webscraping jedoch durch die For-Schleifen, Informationen, die nicht immer an derselben Stelle stehen und Teile, wie die Seitenzahl, die sich nicht scrapen ließen.

Das Webscraping von Immowelt sollte 1x täglich auf dem aws Server EC2 per Putty automatisch laufen. Dieser Vorgang wurde jedoch häufig unterbrochen, weshalb wir das Update manuell auf dem eigenen Rechner gestartet haben.

Außerdem wurden die Bauzinsen von Comdirect gescrapet. Dabei wurde je nach Sollzinsbindung unterschieden. Auch hier wurde per xpath gescrapt und es gab keine Komplikationen.

Das tägliche Scraping von Comdirect llief auch problemlos auf dem aws Server und gab je eine csv-Datei aus.

Die Daten der Häuser von Immowelt wurden dann in diesem separaten Notebook eingelesen und analysiert. Zu Beginn waren es 55060 Zeilen und 16 Spalten, welche von Sonderzeichen, sowie unnötigen Buchstaben und Spalten befreit wurden und daraufhin in den korrekten Datentyp umgewandelt wurden. Danach wurden Nullwerte ersetzt und für die Regression Duplikate entfernt. Dieses gekürzte Dataframe wurde dann explorativ analysiert, zuerst einzelne Features und darauffolgend Zusammenhänge zwischen diesen. Erstaunlich war, dass es keine starke Korrelation einzelner Features zum Preis gibt. Außerdem sind bei fast allen Variablen viele Ausreißer vorhanden. Eine weitere interessante Erkenntnis war, dass Häuser in Leipzig besonders günstig und in München besonders teuer sind, in allen anderen Städten sich die Preise jedoch in einem ähnlichen Rahmen bewegen.

Nach diesen Erkenntnissen, wurden die Daten für die Regression vorbereitet. Die kategorialen Fetures wurden One-Hot-Encoded und die numerischen Features standardisiert. Das Dataframe außerdem in X und y (zu bestimmender Preis), sowie Trainings- und Testdaten unterteilt. Mit diesen Daten wurden mehrere Regressionsmodelle getestet, optimiert und verglichen. Bei den Modellen handelt es sch um lineare Regression mit statsmodel, sowie Lineare Regression, Bayesian Ridge, Support Vector, Stochastic Gradient Descent und Decision Trees mit sklearn. Die Modellperformance aller Modelle ist leider nicht besonders gut, also der Hauspreis wird nicht zuverlässig vorhergesagt. Außerdem sind manche R2 Werte negativ, obwohl diese zwischen 0 und 1 liegen sollten. Am besten war der Decision Tree mit R2 von 0,43.

Anhand des großden Dataframes mit Duplikaten wurde die Inventardauer der Exposes bestimmt. Es wurde gezählt wie häufig diese Vorkommen und beim Betrachtungszeitraum von 8 Tagen, waren die Häuser durchschnittlich über 7 Tage inseriert.

Zuletzt wurden die Hauspreise mit den Bauzinsen angereichert. Die aufgestellt Hypothese war, dass die Hauspreise mit steigenden Zinsen fallen und vice versa. Im Betrachtungszeitraum hat sich diese Hypothese jedoch nciht bewahrheitet, stattdessen konnte kein Zusammenhang zwischen Hauspreisen und Bauzinsen festgestellt werden.